查看原文
其他

【第1761期】一个前端的 functor,applicative functor,monad 初探

Gloria 前端早读课 2019-10-31

前言

今日早读文章由酷家乐@Gloria投稿分享。

正文从这开始~~

在使用 ramda 的时候,经常会在文档中出现一些概念性的名词,比如 functor,applicative functor,monad。这些“高端词汇”都是啥意思,宝宝很好奇,索性就来探一探。

其实解释这些概念,更接近本质方式是数学形式的推导,但是由于水平和精力有限,这篇文章会结合 haskell 中的定义,探索它们在 javascript 和 ramda 库中的体现。

Functor

先来看一看在 haskell 中是如何定义一个函子的,任何可以被 fmap (a -> b)映射的类型实例,该类型就是函子。(其实看不懂也无所谓~)

  1. class Functor f where

  2. fmap :: (a -> b) -> f a -> f b

数组

先来个简单的例子,在 ramda 中如何实现 Array.prototype.map 的功能。

可以使用 R.map ,像下面这样

  1. const a = [1, 2, 3];

  2. const b = R.map(String, a); // ['1', '2', '3']

换一个角度想这个过程:a 是一个容器,里面有3个值,通过函数 R.map(String) 映射出了另外一个同样有3个值的容器 b。

a容器和b容器其实就是 functor,简单地说就是可被映射的容器就是 functor,函子是可包含值的容器。

函数

其实函数也是 functor,可将函数视为包裹着值的容器。

恒值函数

先看一个比较简单的函数,() => 1,这个是一个包裹着值 1 的容器,我们可以将这个容器映射为一个包裹着 2 的容器:

  1. const a = () => 1;

  2. const b = R.map(x => x+1, a); // () => 2

恒值函数是一个包裹着恒定值的函数,利用 ramda 我们可以像下面一样创建这样一个容器:

  1. R.always(2); // () => 2;

在日常开发中,我们接触到的大多数函数都不是恒值函数,而是普通函数,接下来说一说普通函数。

普通函数

普通函数也是函子,可以想象 x => x + 1, 我们可以把这个函数视为一个容器,这个容器的值就是 参数 + 1 ,一个不确定的值而已。

既然我们可以把函数视为一个 包含不确定值的容器。

思考一个问题:如何将 x => x + 1 这样一个容器,映射成 x => (x + 1) * 2 ?

你可能会写出下面这样的代码:

  1. const a = x => x + 1;

  2. const b = x => a(x) * 2;

  3. b(1); // 4

换成 ramda 的写法 :

  1. const a = x => x + 1;

  2. const b = R.map(x => x * 2, a);

  3. b(1); // 4

容器 a 映射成了 b ,映射过程:生成了一个 b 容器,它的值是 a中不确定的值*2。

接下来我们来看一看 functor 的升级版 applicative functor。

Applicative Functor

先看一看 haskell 中的定义:

  1. class Functor f => Applicative f where

  2. pure :: a -> f a

  3. (<*>) :: f (a -> b) -> f a -> f b

首先 Applicative Functor 必须是 Functor ,另外存在 pure 方法接收一个值 a ,返回一个包裹了 a 的容器。

其次,支持 <*> 函数,在 Haskell 中可以像下面这样操作 applicative functor :

  1. pure (+10) <*> Just 9 --> Just 19

  2. --> 或者使用 applicative 风格

  3. (+10) <$> <*> Just 9 --> Just 19

R.ap

在 ramda 中数组和函数同样也都是 applicative functor,我们可以利用 R.ap 函数充当 Haskell 中 <*> 的角色。

先来看一看 R.ap 函数的通用定义,是不是和 applicative functor 的 <*> 函数一样:

  1. Apply f => f (a → b) → f a → f b

函数

有一个包裹着映射规则 x => x + 1 的容器 a 和包裹着值 2 的容器 b。

思考:如何将 a 中的映射规则应用到容器 b 的值上,并将产生的新值放到一个新的容器里。

我们可以利用 R.ap 函数:

  1. const a = R.always(x => x + 1); // 容器 a

  2. const b = R.always(2); // 容器 b

  3. const c = R.ap(a, b); // 容器 c:() => 3

数组

数组也是类似,在数组中包裹一个函数 x => x + 1 ,然后在另一个数组上应用这个数组。

  1. const a = [1];

  2. const b = [x => x + 1];

  3. R.ap(b, a); // [2]

如果数组 a 中有 a 个元素,数组 b 中有 b 个元素,那么最终会生成 a*b 个元素的数组

  1. const a = [1, 2];

  2. const b = [x => x + 1, x => x * 2];

  3. R.ap(b, a); // [2, 2, 3, 4]

见识到 applicative functor 之后,接下来感受一下 Monad 是什么。

Monad

Haskell 中 Monad 定义:

  1. class Monad m where

  2. return :: a -> m a

  3. (>>=) :: m a -> (a -> m b) -> m b

简单来说,首先 return 函数接收一个值,返回一个包裹这个值的容器。其次对于 >>= 函数来说,接收一个容器和对其中值的映射规则,返回一个新的容器,这个容器就是映射规则返回的容器。

换一种角度,其实 Monad 就是在说某种特殊的函数满足结合率这件事,>>= 函数描述了 m 这种容器在 >>= 下满足结合率的这一特性,然后 return 在其中扮演单位元的角色,任何容器通过 >>= 函数与 return 组合,返回的一定是自身。

在 javascript 中,最典型的 monad 就是 Promise

Promise

先创建一个 Promise

  1. const getUser = new Promise(...);

我们创建了一个 Promise 实例。那么如何去消费这个实例产出的值呢?大家都知道,可以用 then 方法:

  1. const consumer = user => {...};

  2. getUser.then(consumer);

在 consumer 函数中去消费 user。如果我们想在拿到 user 之后再去进行异步操作怎么办?改写上面的例子

  1. const getDetailByUser = user => new Promise(...);

  2. getUser.then(getDetailByUser).then(...);

我们再来简化地描述这个过程:

  1. getUser :: Promise<user>

  2. getDetailByUser :: user -> Promise<detail>

把 Promise 看做一个包裹着未来值的容器 m,把 user 用 a 代替,detail 用 b 代替,省去 <>

  1. getUser :: m a

  2. getDetailByUser :: a -> m b

其中 then 方法是不是就有些类似 >>= 函数。

  1. then :: m a ->(a -> m b)-> m b

在 then 函数的作用下类似 a -> m b 这样的过程满足结合率,那么 return 的单位元的职责是由谁来承担的呢?

  1. getUser.then(Promise.resolve).then(getDetailByUser)

  2. getUser.then(getDetailByUser).then(Promise.resolve)

这两行代码的效果都是一样的,Promise.resolve 承担着单位元的作用,任何过程 a->Promise<b> 与 Promise.resolve 通过 then 组合(左结合或右结合),其结果都是 a->Promise<b>

通过对比在 Haskell 中的定义, Promise 符合 Monad 的形式。

通常 monad 模式会用来封装一些副作用,使得这部分"不纯"的逻辑与外部“纯洁”的逻辑隔离开,比如在 haskell 中的 IO 类型。

这篇文章算是给前段时间的困惑画上了一个句号吧,如果有些地方理解有误,还请多多指出。

参考资料:

  • 《 Haskell 趣学指南 》

  • IO and monads

  • 写给小白的Monad指北

关于本文 作者:@Gloria 原文:https://zhuanlan.zhihu.com/p/88741757

他曾分享过


【第1304期】聊一聊Redux的前身Flux


【第1265期】那些前端MVVM框架是如何诞生的


【第1252期】Webpack基本架构浅析

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存